package com.gitee.wsl.net.url

/**
 * Represents an immutable URL
 *
 * @property protocol
 * @property host name without port (domain)
 * @property port the specified port or protocol default port
 * @property specifiedPort port number that was specified to override protocol's default
 * @property encodedPath encoded path without query string
 * @property parameters URL query parameters
 * @property fragment URL fragment (anchor name)
 * @property user username part of URL
 * @property password password part of URL
 * @property trailingQuery keep trailing question character even if there are no query parameters
 */
 class Url internal constructor(
    val protocol: URLProtocol,
    val host: String,
    val specifiedPort: Int,
    val pathSegments: List<String>,
    val parameters: Parameters,
    val fragment: String,
    val user: String?,
    val password: String?,
    val trailingQuery: Boolean,
    private val urlString: String
) {
    init {
        require(
            specifiedPort in 0..65535 || specifiedPort == DEFAULT_PORT
        ) { "port must be between 0 and 65535, or $DEFAULT_PORT if not set" }
    }

     val port: Int get() = specifiedPort.takeUnless { it == DEFAULT_PORT } ?: protocol.defaultPort

     val encodedPath: String by lazy {
        if (pathSegments.isEmpty()) {
            return@lazy ""
        }
        val pathStartIndex = urlString.indexOf('/', protocol.name.length + 3)
        if (pathStartIndex == -1) {
            return@lazy ""
        }
        val pathEndIndex = urlString.indexOfAny(charArrayOf('?', '#'), pathStartIndex)
        if (pathEndIndex == -1) {
            return@lazy urlString.substring(pathStartIndex)
        }
        urlString.substring(pathStartIndex, pathEndIndex)
    }

     val encodedQuery: String by lazy {
        val queryStart = urlString.indexOf('?') + 1
        if (queryStart == 0) return@lazy ""

        val queryEnd = urlString.indexOf('#', queryStart)
        if (queryEnd == -1) return@lazy urlString.substring(queryStart)

        urlString.substring(queryStart, queryEnd)
    }

     val encodedPathAndQuery: String by lazy {
        val pathStart = urlString.indexOf('/', protocol.name.length + 3)
        if (pathStart == -1) {
            return@lazy ""
        }
        val queryEnd = urlString.indexOf('#', pathStart)
        if (queryEnd == -1) {
            return@lazy urlString.substring(pathStart)
        }
        urlString.substring(pathStart, queryEnd)
    }

     val encodedUser: String? by lazy {
        if (user == null) return@lazy null
        if (user.isEmpty()) return@lazy ""
        val usernameStart = protocol.name.length + 3
        val usernameEnd = urlString.indexOfAny(charArrayOf(':', '@'), usernameStart)
        urlString.substring(usernameStart, usernameEnd)
    }

     val encodedPassword: String? by lazy {
        if (password == null) return@lazy null
        if (password.isEmpty()) return@lazy ""
        val passwordStart = urlString.indexOf(':', protocol.name.length + 3) + 1
        val passwordEnd = urlString.indexOf('@')
        urlString.substring(passwordStart, passwordEnd)
    }

     val encodedFragment: String by lazy {
        val fragmentStart = urlString.indexOf('#') + 1
        if (fragmentStart == 0) return@lazy ""

        urlString.substring(fragmentStart)
    }

    override fun toString(): String = urlString

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false

        other as Url

        if (urlString != other.urlString) return false

        return true
    }

    override fun hashCode(): Int {
        return urlString.hashCode()
    }

     companion object
}


/**
 * [Url] authority.
 */
 val Url.authority: String
    get() = buildString {
        append(encodedUserAndPassword)

        if (specifiedPort == DEFAULT_PORT || specifiedPort == protocol.defaultPort) {
            append(host)
        } else {
            append(hostWithPort)
        }
    }

/**
 * A [Url] protocol and authority.
 */
 val Url.protocolWithAuthority: String
    get() = buildString {
        append(protocol.name)
        append("://")
        append(encodedUserAndPassword)

        if (specifiedPort == DEFAULT_PORT || specifiedPort == protocol.defaultPort) {
            append(host)
        } else {
            append(hostWithPort)
        }
    }

internal val Url.encodedUserAndPassword: String
    get() = buildString {
        appendUserAndPassword(encodedUser, encodedPassword)
    }


/**
 * Construct [Url] from [urlString].
 */
@Suppress("FunctionName")
 fun Url(urlString: String): Url = URLBuilder(urlString).build()

fun Url.Companion.parse(urlString: String) = Url(urlString)

/**
 * Construct [Url] from [builder] without building origin.
 */
@Suppress("FunctionName")
 fun Url(builder: URLBuilder): Url = URLBuilder().takeFrom(builder).build()

/**
 * Full encoded path with query string but without domain, port and schema
 */
 val Url.fullPath: String
    get() = buildString { appendUrlFullPath(encodedPath, encodedQuery, trailingQuery) }

/**
 * Host:port pair, not normalized so port is always specified even if the port is schema's default
 */
 val Url.hostWithPort: String get() = "$host:$port"

internal fun Appendable.appendUrlFullPath(
    encodedPath: String,
    encodedQuery: String,
    trailingQuery: Boolean
) {
    if (encodedPath.isNotBlank() && !encodedPath.startsWith("/")) {
        append('/')
    }

    append(encodedPath)

    if (encodedQuery.isNotEmpty() || trailingQuery) {
        append("?")
    }

    append(encodedQuery)
}

 fun Appendable.appendUrlFullPath(
     encodedPath: String,
     encodedQueryParameters: ParametersBuilder,
     trailingQuery: Boolean
) {
    if (encodedPath.isNotBlank() && !encodedPath.startsWith("/")) {
        append('/')
    }

    append(encodedPath)

    if (!encodedQueryParameters.isEmpty() || trailingQuery) {
        append("?")
    }

    encodedQueryParameters.entries()
        .flatMap { (key, value) ->
            if (value.isEmpty()) listOf(key to null) else value.map { key to it }
        }
        .joinTo(this, "&") {
            val key = it.first
            if (it.second == null) {
                key
            } else {
                val value = it.second.toString()
                "$key=$value"
            }
        }
}

/**
 * Checks if [Url] has absolute path.
 */
 val Url.isAbsolutePath: Boolean get() = pathSegments.firstOrNull() == ""

/**
 * Checks if [Url] has absolute path.
 */
 val Url.isRelativePath: Boolean get() = !isAbsolutePath


val Url.scheme:String get() = this.protocol.name


val Url.path:String get() = fullPath.substring(0, fullPath.lastIndexOf("/") + 1)


internal fun StringBuilder.appendUserAndPassword(encodedUser: String?, encodedPassword: String?) {
    if (encodedUser == null) {
        return
    }
    append(encodedUser)

    if (encodedPassword != null) {
        append(':')
        append(encodedPassword)
    }

    append("@")
}